Solving Scenario-Based Python Programming and Debugging Problems
Course: Python Training
Topic: Solving Scenario-Based Python Programming and Debugging Problems
Course Link: Python Training Course at JustAcademy
Demo Registration: Register for a Course Demo
Website: Visit JustAcademy
1. Introduction to Scenario-Based Python Programming
Scenario-based Python programming involves solving practical problems that resemble situations encountered in real applications. Instead of writing code only to demonstrate a language feature, the developer must understand a requirement, identify constraints, select an appropriate solution, and verify that the program behaves correctly.
Debugging is the process of finding, understanding, and correcting defects in a program. Scenario-based debugging combines technical knowledge with logical investigation. A developer might need to fix an incorrect invoice total, investigate a missing API response, resolve a file-processing error, or identify why a program fails for empty input.
These skills are useful in software development, automation, web applications, data processing, testing, and technical interviews.
Learning Objectives
- Analyze real-world programming scenarios and convert requirements into algorithms.
- Identify syntax, runtime, logical, and data-related errors.
- Use Python exceptions and tracebacks to investigate failures.
- Debug functions, loops, conditional statements, collections, and file operations.
- Validate user input and handle unexpected situations safely.
- Write test cases for normal inputs, boundary conditions, and invalid inputs.
- Use logging, assertions, and debugging tools effectively.
- Improve code readability, reliability, and performance.
- Practice interview-style debugging questions and mini-projects.
2. A Systematic Approach to Solving Scenarios
A structured approach prevents developers from making random code changes without understanding the actual problem.
Step 1: Understand the Requirement
Read the scenario carefully and identify the expected behavior. Separate the stated requirements from assumptions that have not been confirmed.
Example scenario: A shopping application calculates the total price of products, but the amount displayed at checkout is incorrect.
Before changing the code, clarify:
- Are product prices stored as integers, floating-point values, or decimal amounts?
- Are quantities being multiplied correctly?
- Should discounts be applied before or after tax?
- Are shipping fees included?
- How should empty carts and invalid quantities be handled?
Step 2: Identify Inputs, Processing, and Outputs
| Component |
Description |
Example |
| Input |
Data received by the program. |
Product prices, quantities, discount rate, tax rate. |
| Processing |
Operations performed on the data. |
Calculate subtotal, discount, and tax. |
| Output |
Expected result. |
Final invoice total. |
| Constraints |
Rules the solution must satisfy. |
Quantities must be positive integers. |
| Error conditions |
Unexpected or invalid situations. |
Negative price or malformed input. |
Step 3: Reproduce the Problem
Run the program using the input that caused the issue. If the original input is unavailable, construct a small example that demonstrates the same behavior.
A bug that cannot be reproduced reliably is difficult to diagnose. Record the input values, error message, expected result, actual result, and relevant execution steps.
Step 4: Find the Root Cause
Trace the data through the program. Inspect function arguments, variable values, conditional branches, loops, and returned results. Focus on identifying the first point where actual behavior differs from expected behavior.
Step 5: Implement and Verify the Fix
Make the smallest appropriate change, then rerun the original failing example and related tests. A successful fix should correct the reported issue without introducing regressions elsewhere.
3. Understanding Common Types of Python Errors
Correctly identifying the type of error helps narrow down the cause and select the appropriate debugging technique.
| Error Type |
Meaning |
Example |
Typical Solution |
| Syntax error |
The code does not follow Python grammar. |
if x > 5 print(x) |
Correct the syntax and indentation. |
| Runtime exception |
An operation fails during execution. |
10 / 0 |
Validate conditions or handle the exception appropriately. |
| Logical error |
The program runs but produces an incorrect result. |
Using addition instead of multiplication for a total. |
Review the algorithm and expected calculations. |
| Type error |
An operation is incompatible with the supplied types. |
"10" + 5 |
Convert or validate types where appropriate. |
| Name error |
A name cannot be resolved in the current scope. |
Using an undefined variable. |
Check spelling, scope, and initialization. |
| Index error |
A sequence index is outside its valid range. |
[1, 2][5] |
Check collection length and index boundaries. |
| Key error |
A dictionary key is missing. |
{"name": "Aarav"}["age"] |
Validate the key or use an appropriate lookup method. |
| Attribute error |
An object does not provide the requested attribute or method. |
Calling a string method on a list. |
Inspect the object's type and available methods. |
Example: Correcting a Type Error
# Incorrect
age = input("Enter your age: ")
next_year_age = age + 1
# Correct
age = int(input("Enter your age: "))
next_year_age = age + 1
print("Next year:", next_year_age)
The input() function returns a string. Converting the input to an integer allows numeric addition. In a production program, invalid input should also be handled instead of allowing an unhandled ValueError.
4. Reading Tracebacks and Locating Errors
A traceback shows the sequence of function calls leading to an unhandled exception. It typically identifies the file, line number, function, exception type, and error message.
def calculate_average(numbers):
return sum(numbers) / len(numbers)
scores = []
print(calculate_average(scores))
This example raises a ZeroDivisionError because the list is empty and its length is zero.
How to Investigate a Traceback
- Read the final line to identify the exception type and message.
- Locate the line where the exception occurred.
- Inspect the values used by that line.
- Follow the preceding function calls if the problem originated elsewhere.
- Determine whether the issue is caused by invalid input, incorrect assumptions, or faulty logic.
- Correct the cause and rerun the failing scenario.
Corrected Version
def calculate_average(numbers):
if not numbers:
raise ValueError("Cannot calculate the average of an empty list")
return sum(numbers) / len(numbers)
print(calculate_average([70, 80, 90])) # 80.0
Raising a clear exception makes the invalid condition explicit. Alternatively, an application may return a documented fallback value if that behavior is part of its requirements.
5. Debugging Techniques for Python Programs
5.1 Using print() for Debugging
The print() function is a simple way to inspect variables and understand how a program executes. It is particularly useful when learning Python or investigating a small function.
def calculate_discount(price, discount_percent):
print("Original price:", price)
print("Discount percentage:", discount_percent)
discount = price * discount_percent / 100
print("Calculated discount:", discount)
final_price = price - discount
print("Final price:", final_price)
return final_price
calculate_discount(1000, 10)
By displaying intermediate values, you can determine whether the error occurs in the input, discount calculation, or final subtraction.
Limitation: Remove temporary debugging output from production code when it is no longer needed. Use structured logging when the application needs persistent diagnostic information.
5.2 Using Python's pdb Debugger
The built-in pdb module allows you to pause execution, inspect variables, step through code, and investigate the program state without adding many print statements.
import pdb
def calculate_total(prices):
pdb.set_trace()
total = 0
for price in prices:
total += price
return total
print(calculate_total([100, 200, 300]))
When the debugger stops at pdb.set_trace(), you can use commands such as:
| Command |
Purpose |
n |
Execute the next line without stepping into a called function. |
s |
Step into a function call. |
c |
Continue execution until the next breakpoint or program stop. |
p variable |
Print the value of an expression or variable. |
l |
Display source code around the current location. |
where |
Display the current call stack. |
q |
Quit the debugger. |
Use the debugger when a problem depends on several variables, complex conditions, or unexpected changes during execution.
5.3 Using Logging Instead of Excessive Print Statements
The logging module records events with severity levels. Logging is useful for applications that need diagnostic information without displaying every message directly to the user.
import logging
logging.basicConfig(
level=logging.DEBUG,
format="%(levelname)s: %(message)s"
)
def calculate_total(prices):
logging.debug("Received prices: %s", prices)
total = sum(prices)
logging.info("Total calculated: %s", total)
return total
print(calculate_total([100, 200, 300]))
Common logging levels include:
DEBUG — detailed diagnostic information.
INFO — normal application events.
WARNING — unexpected conditions that do not necessarily stop execution.
ERROR — a failure that prevents an operation from completing.
CRITICAL — a severe failure that may prevent the application from continuing.
For additional information, read the official Python Logging HOWTO.
6. Handling Exceptions Correctly
Exceptions represent conditions that interrupt the normal execution of a statement or operation. Appropriate exception handling allows a program to respond to expected failures without hiding unrelated programming defects.
6.1 Using try and except
def divide_numbers(first, second):
try:
return first / second
except ZeroDivisionError:
print("Cannot divide by zero.")
return None
print(divide_numbers(10, 2)) # 5.0
print(divide_numbers(10, 0)) # None
The try block contains the operation that might fail. The except block handles the specified exception. Returning None is appropriate only if the calling code understands that it represents a failed calculation.
6.2 Handling Multiple Specific Exceptions
def get_integer(value):
try:
return int(value)
except ValueError:
print("The value must represent an integer.")
return None
print(get_integer("25")) # 25
print(get_integer("hello")) # None
Catch the narrowest appropriate exception. Avoid using a broad except: clause because it may suppress unexpected errors and make debugging more difficult.
6.3 Using else and finally
def read_number(text):
try:
number = int(text)
except ValueError:
print("Invalid integer.")
return None
else:
print("Conversion successful.")
return number
finally:
print("Conversion attempt completed.")
print(read_number("42"))
try contains the operation that might raise an exception.
except handles a matching exception.
else runs when the try block completes without an exception.
finally runs when control leaves the statement, whether or not an exception occurred.
Use finally for cleanup operations when needed. Context managers such as with open(...) are usually preferable for managing files and similar resources.
6.4 Raising Meaningful Exceptions
def calculate_percentage(score, total):
if total <= 0:
raise ValueError("Total must be greater than zero")
if not 0 <= score <= total:
raise ValueError("Score must be between zero and total")
return score / total * 100
print(calculate_percentage(45, 50)) # 90.0
Raising a meaningful exception makes incorrect assumptions explicit. It also helps other developers understand what went wrong and where to investigate.
For more examples, see the official Python Errors and Exceptions tutorial.
7. Scenario-Based Problem: Incorrect Shopping Cart Total
Scenario: An online shopping application calculates an incorrect total when customers buy multiple products. The developer must identify the calculation problem and implement a reliable solution.
Incorrect Implementation
def calculate_cart_total(items):
total = 0
for item in items:
total = item["price"] + item["quantity"]
return total
cart = [
{"price": 100, "quantity": 2},
{"price": 50, "quantity": 3}
]
print(calculate_cart_total(cart))
Problems:
- The code adds price and quantity instead of multiplying them.
- The variable
total is overwritten for each item instead of accumulating the item totals.
- The function does not validate prices or quantities.
- The function does not define how an empty cart should be handled.
Corrected Implementation
def calculate_cart_total(items):
total = 0
for item in items:
price = item["price"]
quantity = item["quantity"]
if price < 0:
raise ValueError("Price cannot be negative")
if not isinstance(quantity, int) or isinstance(quantity, bool):
raise TypeError("Quantity must be an integer")
if quantity < 0:
raise ValueError("Quantity cannot be negative")
total += price * quantity
return total
cart = [
{"price": 100, "quantity": 2},
{"price": 50, "quantity": 3}
]
print(calculate_cart_total(cart)) # 350
Expected calculation: The first product costs 100 × 2 = 200. The second product costs 50 × 3 = 150. The combined total is 350.
This version demonstrates the importance of verifying the formula, accumulating values correctly, and validating data. A real shopping application should also validate the price type, handle missing dictionary keys, and use an appropriate monetary representation such as Decimal when exact decimal arithmetic is required.
8. Scenario-Based Problem: Processing User Input
Scenario: A program asks users to enter their age. If the user types letters or a negative number, the application should display a useful message rather than terminate unexpectedly.
Solution
def validate_age(value):
try:
age = int(value)
except ValueError:
return "Please enter a valid whole number."
if age < 0:
return "Age cannot be negative."
if age > 120:
return "Please enter an age within the supported range."
return f"Accepted age: {age}"
print(validate_age("25"))
print(validate_age("hello"))
print(validate_age("-5"))
print(validate_age("150"))
Key Learning Points
- Convert input to the required data type.
- Handle conversion errors separately from business-rule violations.
- Validate acceptable boundaries explicitly.
- Return predictable results for each documented condition.
- Test valid values, invalid text, negative values, and boundary values.
9. Scenario-Based Problem: Finding Duplicate Records
Scenario: A data-processing script receives a list of customer email addresses. Duplicate addresses must be identified so that the application can flag records for review.
Solution
def find_duplicates(values):
seen = set()
duplicates = set()
for value in values:
normalized = value.strip().lower()
if normalized in seen:
duplicates.add(normalized)
else:
seen.add(normalized)
return sorted(duplicates)
emails = [
"[email protected]",
"[email protected]",
" [email protected] ",
"[email protected]",
"[email protected]"
]
print(find_duplicates(emails))
# ['[email protected]', '[email protected]']
Explanation:
- Normalize each email by trimming whitespace and converting it to lowercase.
- Store previously encountered values in a set.
- If a normalized value has already been seen, add it to the duplicates set.
- Return the sorted duplicate values for predictable output.
Complexity: Average O(n) time for the set-based scan, plus the cost of sorting the distinct duplicates. The example demonstrates normalization for duplicate detection; a production system should apply the business's actual rules for email comparison.
10. Scenario-Based Problem: Handling Missing Files
Scenario: A Python automation script reads a report from disk. Sometimes the file has been moved or deleted, and the application should report the problem clearly.
Solution
from pathlib import Path
def read_report(file_path):
path = Path(file_path)
try:
return path.read_text(encoding="utf-8")
except FileNotFoundError:
print(f"Report not found: {path}")
return None
except PermissionError:
print(f"Permission denied: {path}")
return None
except OSError as error:
print(f"Unable to read report: {error}")
return None
content = read_report("report.txt")
if content is not None:
print(content)
This example distinguishes a missing file, a permissions problem, and other operating-system errors. The calling code checks whether reading succeeded before processing the result.
Debugging Checklist for File Problems
- Verify that the path is correct.
- Check whether a relative path is interpreted from the expected working directory.
- Confirm that the file exists and is accessible.
- Check the file encoding.
- Ensure that the file is not being modified unexpectedly by another process.
- Use a context manager when working with file handles directly.
11. Scenario-Based Problem: Debugging a Data Transformation Function
Scenario: A report contains sales amounts as strings, including empty values. The application needs to calculate a total without crashing on blank records.
Solution
def calculate_sales_total(values):
total = 0.0
invalid_values = []
for index, value in enumerate(values):
if value is None or str(value).strip() == "":
continue
try:
amount = float(value)
except (ValueError, TypeError):
invalid_values.append((index, value))
continue
total += amount
return total, invalid_values
sales = ["120.50", "80", "", None, "invalid", "45.25"]
total, invalid = calculate_sales_total(sales)
print("Total:", total)
print("Invalid records:", invalid)
Expected total: 245.75.
The function skips blank values, records values that cannot be converted, and continues processing the remaining records. Whether invalid rows should be skipped, rejected, or sent for manual review depends on the reporting requirements.
Important: For financial applications, consider using Decimal instead of float when exact decimal calculations are required.
12. Scenario-Based Problem: Debugging API Response Handling
Scenario: A Python application requests information from a web API. The response may contain a successful result, an HTTP error, invalid JSON, or a network failure.
Example Using requests
import requests
def fetch_data(url):
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
return response.json()
except requests.Timeout:
print("The request timed out.")
except requests.HTTPError as error:
print("The server returned an HTTP error:", error)
except requests.ConnectionError:
print("Could not connect to the server.")
except requests.RequestException as error:
print("Request failed:", error)
except ValueError:
print("The response could not be decoded as JSON.")
return None
# Example usage:
# data = fetch_data("https://api.example.com/items")
# if data is not None:
# print(data)
Install the external requests package before running this example if it is not already available: python -m pip install requests.
Debugging Steps
- Confirm that the URL and request parameters are correct.
- Check whether the server is reachable.
- Inspect the HTTP status code and response body.
- Confirm that the response contains valid JSON.
- Use a timeout so that the program does not wait indefinitely.
- Handle errors without exposing authentication tokens or private response data in logs.
Do not treat every HTTP response as a successful result. An API client should check the response status and validate the structure of returned data before using it.
13. Unit Testing Scenario-Based Solutions
Unit testing verifies that small, individual parts of an application behave as expected. Tests help reproduce bugs, verify fixes, and prevent previously corrected problems from returning.
13.1 Writing Tests with unittest
import unittest
def calculate_total(prices):
if not prices:
return 0
return sum(prices)
class TestCalculateTotal(unittest.TestCase):
def test_multiple_prices(self):
self.assertEqual(calculate_total([100, 200, 50]), 350)
def test_single_price(self):
self.assertEqual(calculate_total([75]), 75)
def test_empty_list(self):
self.assertEqual(calculate_total([]), 0)
def test_negative_price_is_summed(self):
self.assertEqual(calculate_total([100, -10]), 90)
if __name__ == "__main__":
unittest.main()
The final test reflects the current function's behavior, not necessarily the correct business rule for every application. If negative prices are invalid in the scenario, update the function to reject them and replace the test with one that expects a ValueError.
13.2 Testing Invalid Inputs
import unittest
def validate_quantity(quantity):
if not isinstance(quantity, int) or isinstance(quantity, bool):
raise TypeError("Quantity must be an integer")
if quantity < 0:
raise ValueError("Quantity cannot be negative")
return quantity
class TestQuantity(unittest.TestCase):
def test_valid_quantity(self):
self.assertEqual(validate_quantity(3), 3)
def test_negative_quantity(self):
with self.assertRaises(ValueError):
validate_quantity(-1)
def test_string_quantity(self):
with self.assertRaises(TypeError):
validate_quantity("3")
if __name__ == "__main__":
unittest.main()
Tests should cover normal behavior and the errors that the function is expected to raise. This makes the program's contract explicit.
13.3 Testing Best Practices
- Use descriptive test names that explain the behavior being checked.
- Test one logical behavior at a time.
- Include boundary cases and invalid inputs.
- Keep tests repeatable and independent of execution order.
- Verify results instead of relying only on printed output.
- Re-run the complete relevant test suite after fixing a bug.
14. Using Assertions and Preconditions
An assertion checks an assumption that should be true during development. It is useful for detecting internal logic problems, but it should not replace validation of untrusted input.
def average(numbers):
assert isinstance(numbers, list), "Expected a list"
if not numbers:
raise ValueError("Cannot calculate average of an empty list")
return sum(numbers) / len(numbers)
print(average([10, 20, 30]))
The assertion checks an internal assumption about the argument type. The explicit exception handles the empty-list condition. In reusable application code, raising a clear TypeError for invalid public input may be more appropriate than using an assertion for type validation.
15. Debugging Loops and Conditional Logic
Many logical errors occur because a loop uses the wrong range, a condition is too broad, or a variable is reset at the wrong point.
Example: Incorrect Running Total
# Incorrect
numbers = [10, 20, 30]
total = 0
for number in numbers:
total = number
print(total) # 30, not 60
The variable is overwritten on each iteration. The correct version accumulates each number:
# Correct
numbers = [10, 20, 30]
total = 0
for number in numbers:
total += number
print(total) # 60
Loop Debugging Checklist
- Check the initial value of every counter and accumulator.
- Confirm that the loop's start and stop boundaries are correct.
- Check whether the loop should include the final value.
- Verify that the loop condition eventually becomes false for a
while loop.
- Ensure that
break and continue are used intentionally.
- Confirm that values are accumulated rather than accidentally overwritten.
- Test empty collections and collections with a single element.
16. Debugging Dictionaries and Data Structures
Incorrect assumptions about collections can cause errors such as missing keys, unexpected duplicates, incorrect indexing, or unwanted mutation.
Example: Safely Reading a Dictionary Value
student = {
"name": "Aarav",
"marks": 85
}
# Direct access raises KeyError if "grade" is missing.
grade = student.get("grade", "Not assigned")
print(grade)
Use direct dictionary indexing when the key is required and its absence represents an error. Use get() when a missing key is expected and a default value is meaningful.
Example: Avoiding Mutation During Iteration
numbers = [1, 2, 3, 4, 5, 6]
# Build a new list rather than removing items while iterating.
even_numbers = [
number for number in numbers
if number % 2 == 0
]
print(even_numbers) # [2, 4, 6]
Building a new collection is often clearer than modifying a list while traversing it. If mutation is necessary, use a carefully designed strategy, such as iterating over a copy or collecting items for removal separately.
17. Performance-Related Debugging
A program may produce the correct result but run too slowly or consume too much memory. Performance debugging focuses on identifying bottlenecks before changing the algorithm.
17.1 Measure Execution Time
from time import perf_counter
def calculate_sum(limit):
total = 0
for number in range(limit):
total += number
return total
start = perf_counter()
result = calculate_sum(1_000_000)
elapsed = perf_counter() - start
print("Result:", result)
print(f"Elapsed time: {elapsed:.4f} seconds")
Measure the same workload consistently and avoid drawing conclusions from a single noisy timing result. Use timeit for focused microbenchmarks and profiling tools to investigate larger programs.
17.2 Improve Repeated Lookups
# Less efficient when the list is large and checked repeatedly.
allowed_users = ["alex", "sam", "jordan"]
requested_users = ["sam", "lee", "alex", "lee"]
# Convert to a set for average-case efficient membership checks.
allowed_user_set = set(allowed_users)
approved = [
user for user in requested_users
if user in allowed_user_set
]
print(approved) # ['sam', 'alex']
Choosing an appropriate data structure can reduce repeated work. However, optimization should preserve the required output order and duplicate-handling rules.
18. Scenario-Based Debugging Workflow for Team Projects
In professional software development, debugging often involves collaboration, source control, test environments, and documenting the resolution.
- Record the issue: Describe the expected behavior and actual behavior.
- Collect evidence: Capture relevant error messages, input values, logs, and environment details without exposing secrets.
- Reproduce the failure: Create a reliable test case or minimal example.
- Isolate the cause: Narrow the problem to a function, module, data source, or external dependency.
- Form a hypothesis: Explain why the failure may be happening before modifying the code.
- Test the hypothesis: Use a breakpoint, logging, a focused test, or a small experiment.
- Apply the fix: Make a targeted change and review its possible side effects.
- Run regression tests: Verify the original scenario and related functionality.
- Document the resolution: Record the cause, change, and verification results.
This process creates a clear audit trail and reduces the risk of fixing symptoms while leaving the underlying defect in place.
19. Practical Mini-Project: Expense Tracker with Validation and Debugging
Project objective: Build a small expense tracker that accepts expense records, validates the amount and category, calculates totals, and reports invalid records clearly.
Project Requirements
- Accept a collection of expense records.
- Validate the presence of required fields.
- Reject negative or non-numeric amounts.
- Group valid expenses by category.
- Calculate the overall total and category totals.
- Keep invalid records separate for review.
- Test empty input, malformed records, and valid expenses.
Project Code
from decimal import Decimal, InvalidOperation
def summarize_expenses(records):
category_totals = {}
invalid_records = []
overall_total = Decimal("0.00")
for index, record in enumerate(records):
try:
category = record["category"]
raw_amount = record["amount"]
if not isinstance(category, str) or not category.strip():
raise ValueError("Category must be a non-empty string")
amount = Decimal(str(raw_amount))
if not amount.is_finite():
raise ValueError("Amount must be finite")
if amount < 0:
raise ValueError("Amount cannot be negative")
except (KeyError, TypeError, ValueError, InvalidOperation) as error:
invalid_records.append({
"index": index,
"record": record,
"error": str(error)
})
continue
category = category.strip()
category_totals[category] = (
category_totals.get(category, Decimal("0.00")) + amount
)
overall_total += amount
return {
"overall_total": overall_total,
"category_totals": category_totals,
"invalid_records": invalid_records
}
expenses = [
{"category": "Food", "amount": "250.50"},
{"category": "Travel", "amount": "120.00"},
{"category": "Food", "amount": "75.25"},
{"category": "Shopping", "amount": "-10"},
{"category": "Travel", "amount": "invalid"},
{"category": "Bills", "amount": "300.00"}
]
report = summarize_expenses(expenses)
print("Overall total:", report["overall_total"])
print("Category totals:")
for category, total in report["category_totals"].items():
print(f"{category}: {total}")
print("Invalid records:", len(report["invalid_records"]))
for invalid in report["invalid_records"]:
print(invalid)
Expected Results
- Food total: 325.75.
- Travel total: 120.00.
- Bills total: 300.00.
- Overall valid total: 745.75.
- Two records are rejected: the negative shopping amount and the non-numeric travel amount.
Project Debugging Exercises
- Modify the program to reject records with missing categories.
- Add a test for an empty expense list.
- Write a test that verifies the overall total.
- Add an optional date field and validate its format.
- Export the summary to a JSON file using a serialization strategy that supports decimal values.
- Add logging to record invalid entries without storing sensitive information.
- Investigate what happens if a record is not a dictionary.
20. Common Debugging Mistakes and Their Solutions
| Mistake |
Why It Causes Problems |
Better Approach |
| Changing code without reproducing the bug |
The change may target the wrong cause. |
Reproduce the issue with a minimal input first. |
| Ignoring the traceback |
The actual exception and failing line may be missed. |
Read the exception type, message, and call stack. |
| Catching every exception silently |
Real defects may remain hidden. |
Catch specific exceptions and log or propagate unexpected failures. |
| Testing only successful inputs |
Boundary and invalid-input bugs remain undetected. |
Test empty, extreme, malformed, and duplicate values. |
| Using print statements without a plan |
Excessive output can obscure the relevant values. |
Inspect targeted variables or use a debugger. |
| Fixing the symptom only |
The same underlying defect may occur elsewhere. |
Identify and correct the root cause. |
| Making large changes at once |
It becomes difficult to determine which change caused a new failure. |
Make focused changes and rerun tests. |
| Ignoring regression testing |
A fix may break previously working features. |
Run relevant tests after every significant correction. |
| Logging secrets |
Passwords, tokens, and private data may be exposed. |
Redact sensitive fields and limit access to logs. |
| Optimizing without measurement |
Complexity may increase without improving performance. |
Measure the bottleneck and compare solutions. |
21. Practice Exercises
Beginner-Level Scenarios
- A program crashes when a user enters text instead of a number. Add input validation.
- A function returns the final list element instead of the sum. Identify and correct the accumulator error.
- A loop skips some elements when removing values from a list. Rewrite the logic safely.
- A function fails on an empty list. Define and implement the correct behavior.
- A dictionary lookup raises a
KeyError. Decide whether the missing key should be rejected or assigned a default.
Intermediate-Level Scenarios
- A shopping cart produces incorrect totals for multiple items. Correct the formula and write tests.
- A CSV import skips records with malformed numeric values. Record invalid rows and continue safely.
- An API call sometimes takes too long. Add a timeout and handle connection failures.
- A report contains duplicate customer records. Normalize values and detect duplicates.
- A data transformation function produces different results for empty strings and
None. Define consistent rules and test them.
Advanced Practice Scenarios
- A script becomes slow when searching a large list repeatedly. Profile the code and choose a suitable data structure.
- A file-processing job leaves partially written output after an error. Design a safer write-and-replace process.
- A multi-step function returns incomplete results after an exception. Identify the failure path and define recovery behavior.
- A concurrent program occasionally produces inconsistent totals. Investigate shared mutable state and synchronization.
- A bug appears only in production. Design a minimal reproduction using sanitized input, logs, and environment information.
22. Python Debugging Interview Questions and Answers
Q1. What is debugging?
Answer: Debugging is the systematic process of locating, understanding, and correcting defects in a program.
Q2. What is the difference between a syntax error and a logical error?
Answer: A syntax error prevents code from being parsed correctly. A logical error allows the program to run but produces a result that does not match the requirements.
Q3. What is a traceback?
Answer: A traceback shows the sequence of calls leading to an exception, along with source locations and diagnostic information.
Q4. How does pdb help with debugging?
Answer: The pdb debugger allows developers to pause execution, inspect variables, step through code, and examine the call stack.
Q5. What is the difference between raising and handling an exception?
Answer: Raising an exception signals that an operation cannot proceed normally. Handling an exception means responding to a particular failure with appropriate recovery, reporting, or cleanup behavior.
Q6. Why should broad exception handling be avoided?
Answer: Broad handlers can hide programming defects and make failures harder to diagnose. Catch specific exceptions when the program knows how to handle them.
Q7. What is regression testing?
Answer: Regression testing checks that a code change has not broken functionality that previously worked.
Q8. What are edge cases?
Answer: Edge cases are inputs or conditions at the boundaries of a problem, such as empty collections, zero values, duplicates, and maximum allowed values.
Q9. What is the purpose of logging?
Answer: Logging records application events and diagnostic details that help developers monitor behavior and investigate failures.
Q10. What is root-cause analysis?
Answer: Root-cause analysis identifies the underlying reason for a problem rather than merely correcting its visible symptom.
Q11. How do you debug an incorrect calculation?
Answer: Verify the requirements and formula, inspect input values and intermediate results, test a small example manually, and add tests for boundary conditions.
Q12. How do you debug a slow Python program?
Answer: Measure execution time, use profiling tools to identify bottlenecks, inspect repeated work and data structures, then benchmark an improved implementation.
Q13. Why is unit testing important when fixing bugs?
Answer: Unit tests provide repeatable evidence that the correction works and help prevent the same defect from returning.
Q14. What is the difference between an error message and a log message?
Answer: An error message communicates a failure, often to a developer or user. A log message records an event for diagnosis, monitoring, or later investigation. A program may use both.
Q15. What should you do if a bug cannot be reproduced?
Answer: Collect safe diagnostic information, identify the conditions under which it occurs, examine logs and environment differences, and gradually reduce the scenario until the failure can be reproduced.
23. Best Practices for Reliable Python Code
- Understand the requirement before choosing an implementation.
- Keep functions small, focused, and easy to test.
- Use descriptive names and consistent formatting.
- Validate input at appropriate application boundaries.
- Use specific exceptions and provide useful error messages.
- Avoid silently ignoring unexpected exceptions.
- Use logging for diagnostic events in larger applications.
- Test normal behavior, boundary conditions, and failure scenarios.
- Use automated tests to protect against regressions.
- Measure performance before making optimization changes.
- Protect confidential information in error messages and logs.
- Document assumptions and recovery behavior.
- Use version control to review changes and make debugging easier.
- Consult official documentation when a behavior or exception is unclear.
24. Quick Revision Checklist
- Understand the expected and actual behavior of the program.
- Reproduce the issue with a small, reliable example.
- Identify whether the defect is syntactic, runtime-related, or logical.
- Read the traceback and inspect the relevant variables.
- Use print statements, logging, or pdb where appropriate.
- Validate inputs and handle expected exceptions explicitly.
- Check loop boundaries, conditions, accumulators, and data structures.
- Test empty inputs, invalid values, duplicates, and boundary conditions.
- Correct the root cause rather than hiding the symptom.
- Run unit tests and regression tests after the fix.
- Measure performance before optimizing.
- Record the cause and solution for future maintenance.
25. Conclusion
Solving scenario-based Python programming and debugging problems requires more than knowing Python syntax. It involves understanding requirements, tracing data, selecting suitable algorithms, handling exceptions, validating inputs, and systematically testing solutions.
By practicing real-world scenarios such as incorrect calculations, invalid user input, missing files, duplicate records, API failures, and slow data processing, learners can build the confidence to diagnose problems and write more reliable applications.
Continue developing your Python programming skills through the Python Training Course at JustAcademy. You can also register for a course demo or visit the JustAcademy website.
Additional Learning Resources